import 'package:sqliteasy_core/enums/conflict_strategy.dart';
import 'package:sqliteasy_core/sqliteasy_core.dart' as core;
import 'package:sqliteasy_core/sqliteasy_core.dart' hide ColumnInfo, ForeignKeyInfo, IndexInfo;
import 'package:analyzer/dart/element/element.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert' as convert;

class DatabaseInfo {
  ClassElement element;

  /// The list of entities included in the database. Each entity turns into a table in the
  /// database.
  ///
  /// @return The list of entities in the database.
  List<EntityInfo> entities;

  /// The list of database views included in the database. Each class turns into a view in the
  /// database.
  ///
  /// @return The list of database views.
  List<ViewInfo> views;

  /// The database version.
  ///
  /// @return The database version.
  int version;

  /// You can set the annotation processor argument ({@code room.schemaLocation}) to tell Room to
  /// export the database schema into a folder. Even though it is not mandatory, it is a good
  /// practice to have version history of your schema in your codebase and you should commit the
  /// schema files into your version control system (but don't ship them with your app!).
  /// <p>
  /// When {@code room.schemaLocation} is set, Room will check this variable and if it is set to
  /// {@code true}, the database schema will be exported into the given folder.
  /// <p>
  /// {@code exportSchema} is {@code true} by default but you can disable it for databases when
  /// you don't want to keep history of versions (like an in-memory only database).
  ///
  /// @return Whether the schema should be exported to the given folder when the
  /// {@code room.schemaLocation} argument is set. Defaults to {@code true}.
  bool exportSchema;

  List<MethodElement> daoGetters;

  NullValueStrategy nullValueStrategy;

  DatabaseInfo(this.element, this.entities, this.version,
      {this.exportSchema, this.views, this.daoGetters, this.nullValueStrategy});

  @override
  String toString() {
    return 'DatabaseInfo{\nentities: $entities\n views: $views\n version: $version\n exportSchema: $exportSchema\n}';
  }

  String get identityHash {
    List<TableInfo> tableInfoList = entities.map((e) => e.toTableInfo()).toList();
    tableInfoList.sort((a, b) => a.hashCode.compareTo(b.hashCode));
    StringBuffer identityString = StringBuffer();
    tableInfoList.forEach((element) {
      identityString.write(element.name);

      List<core.ColumnInfo> columnList = element.columns.toList();
      columnList.sort((a, b) => a.name.compareTo(b.name));
      columnList.forEach((columnInfo) {
        identityString.write(columnInfo.toString().replaceAll(RegExp("[\n\\s{},]+"), ""));
      });
      
      List<core.ForeignKeyInfo> foreignKeyList = element.foreignKeys.toList();
      foreignKeyList.sort((a, b) => a.hashCode.compareTo(b.hashCode));
      foreignKeyList.forEach((foreignKey) {
        identityString.write(foreignKey.toString()..replaceAll(RegExp("[\n\\s{},]+"), ""));
      });

      List<core.IndexInfo> indexList = element.indexes.toList();
      indexList.sort((a, b) => a.hashCode.compareTo(b.hashCode));
      indexList.forEach((index) {
        identityString.write(index.toString()..replaceAll(RegExp("[\n\\s{},]+"), ""));
      });
    });
    return md5.convert(convert.utf8.encode(identityString.toString())).toString();
  }
}

class EntityInfo {
  String location;
  String entityName;

  /// The table name in the SQLite database. If not set, defaults to the class name.
  ///
  /// @return The SQLite tableName of the Entity.
  String tableName;

  List<ColumnInfo> columns;

  /// List of indices on the table.
  ///
  /// @return The list of indices on the table.
  List<IndexInfo> indices;

  /// If set to {@code true}, any Index defined in parent classes of this class will be carried
  /// over to the current {@code Entity}. Note that if you set this to {@code true}, even if the
  /// {@code Entity} has a parent which sets this value to {@code false}, the {@code Entity} will
  /// still inherit indices from it and its parents.
  /// <p>
  /// When the {@code Entity} inherits an index from the parent, it is <b>always</b> renamed with
  /// the default naming schema since SQLite <b>does not</b> allow using the same index name in
  /// multiple tables. See {@link Index} for the details of the default name.
  /// <p>
  /// By default, indices defined in parent classes are dropped to avoid unexpected indices.
  /// When this happens, you will receive a {@link RoomWarnings#INDEX_FROM_PARENT_FIELD_IS_DROPPED}
  /// or {@link RoomWarnings#INDEX_FROM_PARENT_IS_DROPPED} warning during compilation.
  ///
  /// @return True if indices from parent classes should be automatically inherited by this Entity,
  ///         false otherwise. Defaults to false.
  bool inheritSuperIndices;

  /// The list of Primary Key column names.
  /// <p>
  /// If you would like to define an auto generated primary key, you can use {@link PrimaryKey}
  /// annotation on the field with [PrimaryKey.autoGenerate] set to [true].
  ///
  /// @return The primary key of this Entity. Can be empty if the class has a field annotated
  /// with {@link PrimaryKey}.
  List<ColumnInfo> primaryKeys;

  /// List of {@link ForeignKey} constraints on this entity.
  ///
  /// @return The list of {@link ForeignKey} constraints on this entity.
  List<ForeignKeyInfo> foreignKeys;

  /// The list of column names that should be ignored by Room.
  /// <p>
  /// Normally, you can use {@link Ignore}, but this is useful for ignoring fields inherited from
  /// parents.
  /// <p>
  /// Columns that are part of an {@link Embedded} field can not be individually ignored. To ignore
  /// columns from an inherited {@link Embedded} field, use the name of the field.
  ///
  /// @return The list of field names.
  List<ColumnInfo> ignoredColumns;

  EntityInfo(this.location, this.entityName, this.tableName, this.columns,
      {this.indices, this.foreignKeys, this.ignoredColumns, this.inheritSuperIndices, this.primaryKeys});

  @override
  String toString() {
    return 'EntityInfo{\ntableName: $tableName\n columns: $columns\n indices: $indices\n inheritSuperIndices: $inheritSuperIndices\n primaryKeys: $primaryKeys\n foreignKeys: $foreignKeys\n ignoredColumns: $ignoredColumns\n}';
  }

  TableInfo toTableInfo() {
    return TableInfo(
      name: tableName,
      columns: this.columns.map(
            (e) => core.ColumnInfo(
                name: e.name,
                type: core.SQLiteTypes.getTypeFromName(e.type),
                notNull: e.notNull ?? false,
                defaultValue: e.defaultValue,
                primaryKeyPosition: e.primaryKeyPosition),
          ).toSet(),
      foreignKeys: this.foreignKeys.map(
            (e) => core.ForeignKeyInfo(
              referenceTable: e.entity.tableName,
              columns: e.columns,
              referenceColumns: e.referenceColumns,
              onUpdate: e.onUpdate ?? Action.NO_ACTION,
              onDelete: e.onDelete ?? Action.NO_ACTION,
            ),
          ).toSet(),
      indexes: this.indices.map(
            (e) => core.IndexInfo(name: e.name, unique: e.unique, columns: e.columns),
          ).toSet(),
    );
  }
}

class ColumnInfo {
  FieldElement field;

  /// The column name.
  String name;

  /// The column type affinity.
  String type;

  /// The column type after it is normalized to one of the basic types according to
  /// https://www.sqlite.org/datatype3.html Section 3.1.
  /// <p>
  /// This is the value Room uses for equality check.
  int affinity;

  /// Whether or not the column can be NULL.
  bool notNull;

  /// The position of the column in the list of primary keys, 0 if the column is not part
  /// of the primary key.
  /// <p>
  /// This information is only available in API 20+.
  /// <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
  /// On older platforms, it will be 1 if the column is part of the primary key and 0
  /// otherwise.
  /// <p>
  /// The {@link #equals(Object)} implementation handles this inconsistency based on
  /// API levels os if you are using a custom SQLite deployment, it may return false
  /// positives.
  int primaryKeyPosition;

  /// The default value of this column.
  String defaultValue;

  bool ignored;

  bool isPrimaryKey;

  bool autoGenerate;

  ColumnInfo(this.field, this.name,
      {this.type,
      this.affinity,
      this.notNull,
      this.primaryKeyPosition,
      this.defaultValue,
      this.ignored,
      this.isPrimaryKey,
      this.autoGenerate});

  @override
  String toString() {
    return 'ColumnInfo{\nname: $name\n type: $type\n affinity: $affinity\n notNull: $notNull\n primaryKeyPosition: $primaryKeyPosition\n defaultValue: $defaultValue\n ignored: $ignored\n}';
  }
}

class IndexInfo {
  /// List of column names in the Index.
  /// <p>
  /// The order of columns is important as it defines when SQLite can use a particular index.
  /// See <a href="https://www.sqlite.org/optoverview.html">SQLite documentation</a> for details on
  /// index usage in the query optimizer.
  ///
  /// @return The list of column names in the Index.
  List<String> columns;

  /// Name of the index. If not set, Room will set it to the list of columns joined by '_' and
  /// prefixed by "index_${tableName}". So if you have a table with name "Foo" and with an index
  /// of {"bar", "baz"}, generated index name will be  "index_Foo_bar_baz". If you need to specify
  /// the index in a query, you should never rely on this name, instead, specify a name for your
  /// index.
  ///
  /// @return The name of the index.
  String name;

  /// If set to true, this will be a unique index and any duplicates will be rejected.
  ///
  /// @return True if index is unique. False by default.
  bool unique;

  IndexInfo(this.columns, {this.name, this.unique});
}

class ForeignKeyInfo {
  /// The parent Entity to reference. It must be a class annotated with {@link Entity} and
  /// referenced in the same database.
  ///
  /// @return The parent Entity.
  EntityInfo entity;

  /// The list of column names in the parent {@link Entity}.
  /// <p>
  /// Number of columns must match the number of columns specified in {@link #childColumns()}.
  ///
  /// @return The list of column names in the parent Entity.
  /// @see #childColumns()
  List<String> referenceColumns;

  /// The list of column names in the current {@link Entity}.
  /// <p>
  /// Number of columns must match the number of columns specified in {@link #parentColumns()}.
  ///
  /// @return The list of column names in the current Entity.
  List<String> columns;

  /// Action to take when the parent {@link Entity} is deleted from the database.
  /// <p>
  /// By default, {@link #NO_ACTION} is used.
  ///
  /// @return The action to take when the referenced entity is deleted from the database.
  Action onDelete;

  /// Action to take when the parent {@link Entity} is updated in the database.
  /// <p>
  /// By default, {@link #NO_ACTION} is used.
  ///
  /// @return The action to take when the referenced entity is updated in the database.
  Action onUpdate;

  /// * A foreign key constraint can be deferred until the transaction is complete. This is useful
  /// if you are doing bulk inserts into the database in a single transaction. By default, foreign
  /// key constraints are immediate but you can change it by setting this field to {@code true}.
  /// You can also use
  /// <a href="https://sqlite.org/pragma.html#pragma_defer_foreign_keys">defer_foreign_keys</a>
  /// PRAGMA to defer them depending on your transaction.
  ///
  /// @return Whether the foreign key constraint should be deferred until the transaction is
  /// complete. Defaults to {@code false}.
  bool deferred;

  ForeignKeyInfo(this.entity, this.columns, this.referenceColumns, {this.deferred, this.onDelete = Action.NO_ACTION, this.onUpdate = Action.NO_ACTION});
}

/// To mark a column ignored.
class IgnoreInfo {}

class ViewInfo {}

class PrimaryKeyInfo {
  bool autoGenerate;
  int position;

  PrimaryKeyInfo(this.autoGenerate, this.position);
}
